import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import javax.swing.filechooser.*;

final class World2D extends World implements MouseListener, MouseMotionListener/*, KeyListener*/ { //Cell Manager
   private int[][] cells;
   private int[][] buffer1;
   private int[][] buffer2;
   //public static final Color[] colors=new Color[] {Color.black, Color.white, Color.gray, Color.gray.darker()};
   public Color[] colors;
   public static final Color[] DEFAULT_COLORS=new Color[] {Color.red, Color.yellow, Color.green, Color.blue, Color.magenta};
   public void changeColors() {
      int r1=(int)(Math.random()*DEFAULT_COLORS.length);
      int r2=(int)(Math.random()*DEFAULT_COLORS.length);
      if(r2==r1) {
         r2=(r2+1)%DEFAULT_COLORS.length;
      }

      Color pick1=DEFAULT_COLORS[r1];
      Color pick2=DEFAULT_COLORS[r2];
      colors=ColorUtility.generateGradient(pick1, pick2, 32);
      /*
      colors=ColorUtility.generateGradient(new Color[] {Color.red, Color.yellow, Color.green, new Color(0x00ffff), Color.blue}, 4);
      */
   }
   {
      changeColors();
   }
   public static int BORDER_SIZE=5;
   public int xOffset=5;
   public int yOffset=5;
   public static Color borderColor=Color.gray;
   public static Color gridColor=Color.gray;
   public static Color backgroundColor=Color.black;
   private static final Random random=new Random(System.currentTimeMillis());

   public World2D(Rule2D r, int p, int q) {
      super(r);
      init(p, q);
      
      addMouseListener(this);
      addMouseMotionListener(this);
      //addKeyListener(this);
      //grabFocus(); //: no use
      //requestFocus(); //: no use
   }
   public void generateRandomMap(double liveRate) {
      for(int i=0; i<cells.length; i++) {
         for(int j=0; j<cells[i].length; j++) {
            cells[i][j]=random.nextDouble()>liveRate?0:1;
         }
      }
   }
   public void shiftRight() {
      if(cells[0].length==1) return;
      final int[] tempColumn=new int[cells.length];
      //save temp
      for(int i=0; i<cells.length; i++) {
         tempColumn[i]=cells[i][cells[0].length-1];
      }
      //shift
      for(int j=cells[0].length-1; j>=1; j--) {
         for(int i=0; i<cells.length; i++) {
            cells[i][j]=cells[i][j-1];
         }
      }
      //restore temp
      for(int i=0; i<cells.length; i++) {
         cells[i][0]=tempColumn[i];
      }
   }
   public void shiftLeft() {
      if(cells[0].length==1) return;
      final int[] tempColumn=new int[cells.length];
      //save temp
      for(int i=0; i<cells.length; i++) {
         tempColumn[i]=cells[i][0];
      }
      //shift
      for(int j=1; j<cells[0].length; j++) {
         for(int i=0; i<cells.length; i++) {
            cells[i][j-1]=cells[i][j];
         }
         
      }
      //restore temp
      for(int i=0; i<cells.length; i++) {
         cells[i][cells[0].length-1]=tempColumn[i];
      }
   }  
   public void shiftUp() {
      if(cells.length==1) return;
      final int[] tempColumn=new int[cells[0].length];
      //save temp
      for(int i=0; i<cells[0].length; i++) {
         tempColumn[i]=cells[0][i];
      }
      //shift
      for(int i=1; i<cells.length; i++) {
         for(int j=0; j<cells[0].length; j++) {
            cells[i-1][j]=cells[i][j];
         }
      }
      //restore temp
      for(int i=0; i<cells[0].length; i++) {
         cells[cells.length-1][i]=tempColumn[i];
      }
   }    
   public void shiftDown() {
      if(cells.length==1) return;
      final int[] tempColumn=new int[cells[0].length];
      //save temp
      for(int i=0; i<cells[0].length; i++) {
         tempColumn[i]=cells[cells[0].length-1][i];
      }
      //shift
      for(int i=cells.length-1; i>=1; i--) {
         for(int j=0; j<cells[0].length; j++) {
            cells[i][j]=cells[i-1][j];
         }
      }
      //restore temp
      for(int i=0; i<cells[0].length; i++) {
         cells[0][i]=tempColumn[i];
      }
   }  
   public void clear() {
      for(int i=0; i<cells.length; i++) {
         for(int j=0; j<cells[i].length; j++) {
            buffer1[i][j]=0;
            buffer2[i][j]=0;
         }
      }
   }
   public void init(int p, int q) {
      if(p<=0||q<=0) throw new IllegalArgumentException();
      buffer1=new int[p][q];
      buffer2=new int[p][q];
      cells=buffer1;
   }
   private void init(int[][] values) {
      buffer1=values;
      buffer2=new int[values.length][values[0].length];
      cells=buffer1;
   }
   public void stepAll() {
      if(cells==buffer1) {
         for(int i=0; i<cells.length; i++) {
            for(int j=0; j<cells[i].length; j++) {
               buffer2[i][j]=step(i, j);
            }
         }
         cells=buffer2;
      } else {
         for(int i=0; i<cells.length; i++) {
            for(int j=0; j<cells[i].length; j++) {
               buffer1[i][j]=step(i, j);
            }
         }
         cells=buffer1;
      }
   }
   public int step(int i, int j) {
      return getRule().apply(cells[i][j], getNeighbors(i, j));
   }
   private void set(int i, int j, int status) { //safe set
      if(i<0||i>=cells.length||j<0||j>=cells[0].length) return;
      cells[i][j]=status;
   }
   private int get(int i, int j) { //safe get
      if(isTorus) {
         int safeI=i<0?
                   cells.length-(-i)%cells.length:
                   i%cells.length;
         int safeJ=j<0?
                   cells[safeI].length-(-j)%cells[safeI].length:
                   j%cells[safeI].length;
//System.out.printf("%d, %d%n", safeI, safeJ);         
         return cells[safeI][safeJ];
      } else {
         if(i>=cells.length||i<0||j>=cells[i].length||j<0) {
            return EMPTY_CELL;
         } else {
            return cells[i][j];
         }
      }
   }
   /*
    * 012
    * 3c4
    * 567
    */
   private int[] getNeighbors(int x, int y) {
      final int[] neighbors=new int[8];
      int index=0;
      for(int i=x-1; i<=x+1; i++) {
         for(int j=y-1; j<=y+1; j++) {
            if(i==x&&j==y) continue;
            neighbors[index++]=get(i, j);
         }
      }
      return neighbors;
   }
   private final java.util.Stack<int[][]> history=new java.util.Stack<int[][]>();
   private final java.util.Stack<int[][]> future=new java.util.Stack<int[][]>();
   /*
   private boolean needLog() {
      if(!history.isEmpty()) {
         int[][] peek=history.peek();
         for(int i=0; i<peek.length; i++) {
            for(int j=0; j<peek[0].length; j++) {
               if(peek[i][j]!=cells[i][j]) return true;
               //System.out.print(peek[i][j]);
            }
            //System.out.println();
         }      
         return false;
      } else {
         return true;
      }
   }
   */
   public void log() {
      //if(!needLog()) return;
      int[][] temp=new int[cells.length][cells[0].length]; //>>> clone() is shallow
      for(int i=0; i<temp.length; i++) {
         for(int j=0; j<temp[0].length; j++) {
            temp[i][j]=cells[i][j];
            //System.out.print(temp[i][j]);
         }
         //System.out.println();
      }      
      history.push(temp);
      future.clear();
   }
   public boolean hasPast() { return !history.isEmpty(); }
   public boolean hasFuture() { return !future.isEmpty(); }
   //model: history-stack(top):current:(top)future-stack
   public void previous() {
      if(!history.isEmpty()) {
         int[][] temp=history.pop();
         future.push(cells);
         init(temp);
      }
   }
   public void next() {
      if(!future.isEmpty()) {
         int[][] temp=future.pop();
         history.push(cells);
         init(temp);
      }
   }

   private int p() {
      return cells.length;
   }
   private int q() {
      return cells[0].length;
   }
   private void drawBackground(Graphics g, int xOffset, int yOffset, double cw, double ch) {
      g.setColor(borderColor);
      g.fillRect(0, 0, getWidth(), getHeight());
      g.setColor(backgroundColor);
      g.fillRect(xOffset, yOffset, (int)(cw*q()), (int)(ch*p()));
      g.drawRect(xOffset, yOffset, (int)(cw*q()), (int)(ch*p()));
   }
   private void drawCells(Graphics g, int xOffset, int yOffset, double cw, double ch) {
      for(int i=0; i<cells.length; i++) {
         for(int j=0; j<cells[i].length; j++) {
            if(cells[i][j]==0) continue;
            int colorIndex=(int)((double)cells[i][j]/getRule().getStateCount()*colors.length);
            g.setColor(colors[colorIndex%colors.length]);
            g.fillRect(xOffset+(int)(j*cw), yOffset+(int)(i*ch), (int)cw+1, (int)ch+1);
            //g.drawRect(xOffset+(int)(j*cw), yOffset+(int)(i*ch), (int)cw, (int)ch);
         }
      }
   } 
   private void drawGrid(Graphics g, int xOffset, int yOffset, double cw, double ch) {
      double w=cw*cells[0].length;
      double h=ch*cells.length;
      g.setColor(gridColor);
      for(int i=1; i<cells.length; i++) {
         g.drawLine(xOffset, (int)(i*ch)+yOffset, (int)(xOffset+w), (int)(i*ch)+yOffset);
      }
      for(int i=1; i<cells[0].length; i++) {
         g.drawLine((int)(i*cw)+xOffset, yOffset, (int)(i*cw)+xOffset, (int)(yOffset+h));
      }
   }
   private void fixDraw(Graphics g) {
      final double size1=(double)(getWidth()-2*BORDER_SIZE)/q();
      final double size2=(double)(getHeight()-2*BORDER_SIZE)/p();
      double cw, ch;
      cw=ch=Math.min(size1, size2);
      xOffset=(int)((getWidth()-cw*q())/2);
      yOffset=(int)((getHeight()-ch*p())/2);
      
      drawBackground(g, xOffset, yOffset, cw, ch);
      drawCells(g, xOffset, yOffset, cw, ch);
      if(showGrid) {
         drawGrid(g, xOffset, yOffset, cw, ch);
      }
      g.dispose();
   }
   public void scaleDraw(Graphics g) {
      xOffset=yOffset=BORDER_SIZE;
      g.setColor(borderColor);
      g.fillRect(0, 0, getWidth(), getHeight());
      int w=this.getWidth()-2*xOffset;
      int h=this.getHeight()-2*yOffset;
      g.setColor(backgroundColor);
      g.fillRect(xOffset, yOffset, w, h);
      double cw=(w/(double)cells[0].length); //>>> 0 may be dangerous
      double ch=(h/(double)cells.length); 

      /*
      g.setColor(Color.gray);
      g.fillRect(xOffset+(int)((cells[0].length/2)*cw), 
                 yOffset+(int)((cells.length/2)*ch), 
                 (int)cw, 
                 (int)ch);
      g.drawRect(xOffset+(int)((cells[0].length/2)*cw), 
                 yOffset+(int)((cells.length/2)*ch), 
                 (int)cw, 
                 (int)ch);
      */

      drawCells(g, xOffset, yOffset, cw, ch);
      if(showGrid) {
         drawGrid(g, xOffset, yOffset, cw, ch);
      }
      g.dispose();
   }
   public void draw(Graphics g) {
      //long start=System.nanoTime();
      if(keepAspectRatio) {
         fixDraw(g);
      } else {
         scaleDraw(g);
      }
      //long period=System.nanoTime()-start;
      //System.out.printf("\r%d      ", period);
   }
   public String toString() {
      return String.format("%s@%dx%d - %s", 
         getRule(), cells[0].length, cells.length, Main.APP_NAME); //>>> 0 may be dangerous

   }
   private int[] cellAt(int x, int y) { //>>> unsure
      int w=this.getWidth()-2*xOffset;
      int h=this.getHeight()-2*yOffset;
      double cw=(w/(double)cells[0].length);
      double ch=(h/(double)cells.length); //>>> 0 may be dangerous
      int p=(int)((y-yOffset)/ch);
      int q=(int)((x-xOffset)/cw);
      return new int[] {p, q};
   }

   public void mousePressed(MouseEvent e) {
   }
   public void mouseClicked(MouseEvent e) {
      int[] pos=cellAt(e.getX(), e.getY());
      //if(get(pos[0], pos[1])!=Rule.LIVE) {
      if(!e.isMetaDown()) { //left button
         set(pos[0], pos[1], Rule2D.LIVE);
      } else {
         set(pos[0], pos[1], Rule2D.DEAD);
      }
      //System.out.printf("click cells[%d][%d]", pos[0], pos[1]);
      repaint();
   }
   public void mouseExited(MouseEvent e) {}
   public void mouseEntered(MouseEvent e) {}
   public void mouseReleased(MouseEvent e) {}
   public void mouseMoved(MouseEvent e) {}
   private boolean isInsideBoundry(int x, int y) {
      if(x>getWidth()-xOffset||x<xOffset||
         y>getHeight()-yOffset||y<yOffset) return false;
      else return true;
   }
   public void mouseDragged(MouseEvent e) {
      if(!isInsideBoundry(e.getX(), e.getY())) return;
      int[] pos=cellAt(e.getX(), e.getY());
      if(!e.isMetaDown()) { //left button
         set(pos[0], pos[1], Rule2D.LIVE);
         //System.out.printf("click cells[%d][%d]", pos[0], pos[1]);
      } else { //right button
         set(pos[0], pos[1], Rule2D.DEAD);
      }
      repaint();
   }
   public void toFile(File file) throws IOException {
      FileWriter writer=null;
      try {
         writer=new FileWriter(file);
         writer.write(toText());
      } catch(IOException e) {
         e.printStackTrace();
      } finally {
         if(writer!=null) writer.close();
      }
   }
   public void fromFile(File file) throws IOException {
      StringBuilder sb=new StringBuilder();
      FileReader reader=null;
      try {
         reader=new FileReader(file);
         int c;
         while((c=reader.read())!=-1) {
            sb.append((char)c);
         }
      } catch(IOException e) {
         e.printStackTrace();
      } finally {
         if(reader!=null) reader.close();
      }
      if(sb.length()==0) return; 
      fromText(sb.toString());
   }
   public String toText() {
      StringBuilder sb=new StringBuilder();
      for(int i=0; i<cells.length; i++) {
         for(int j=0; j<cells[0].length; j++) {
            sb.append((char)(cells[i][j]+48)); 
            //] >>> int to ascii, may produce invalid ascii code
         }
         sb.append("\n");
      }
      return sb.toString();
   }
   public void fromText(String src) {
      final int[] values=new int[src.length()];
      int index=0;
      int row=0;
      for(int i=0; i<src.length(); i++) {
         if(src.charAt(i)=='\n') {
            row++;
         } else {
            values[index++]=src.charAt(i)-48;
         }
      }
      final int column=(values.length-row)/row;
      init(row, column);
      for(int i=0; i<cells.length; i++) {
         for(int j=0; j<cells[0].length; j++) {
            cells[i][j]=values[i*column+j];
         }
      }
   }
   /*
   public void keyPressed(KeyEvent e) {
      if(e.getKeyCode()==KeyEvent.VK_RIGHT) {
         shiftRight();
         repaint();
      }
   }
   public void keyTyped(KeyEvent e) {}
   public void keyReleased(KeyEvent e) {}
   */
}
